// Copyright (c) Microsoft Corporation.  All Rights Reserved.  See License.txt in the project root for license information.

namespace Microsoft.FSharp.Core

open System
open System.Text
open Microsoft.FSharp.Core.LanguagePrimitives.IntrinsicOperators
open Microsoft.FSharp.Core.Operators
open Microsoft.FSharp.Core.Operators.Checked
open Microsoft.FSharp.Collections
open Microsoft.FSharp.Primitives.Basics

[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
[<RequireQualifiedAccess>]
module String =

    /// LOH threshold is calculated from Internal.Utilities.Library.LOH_SIZE_THRESHOLD_BYTES,
    /// and is equal to 80_000 / sizeof<char>
    [<Literal>]
    let LOH_CHAR_THRESHOLD = 40_000

    [<CompiledName("Length")>]
    let length (str: string) =
        if isNull str then 0 else str.Length

    [<CompiledName("Concat")>]
    let concat sep (strings: seq<string>) =

        let concatArray sep (strings: string[]) =
            match length sep with
            | 0 -> String.Concat strings
            // following line should be used when this overload becomes part of .NET Standard (it's only in .NET Core)
            //| 1 -> String.Join(sep.[0], strings, 0, strings.Length)
            | _ -> String.Join(sep, strings, 0, strings.Length)

        match strings with
        | :? (string[]) as arr -> concatArray sep arr

        | :? (string list) as lst -> lst |> List.toArray |> concatArray sep

        | _ -> String.Join(sep, strings)

    [<CompiledName("Iterate")>]
    let iter (action: (char -> unit)) (str: string) =
        if not (String.IsNullOrEmpty str) then
            for i = 0 to str.Length - 1 do
                action str.[i]

    [<CompiledName("IterateIndexed")>]
    let iteri action (str: string) =
        if not (String.IsNullOrEmpty str) then
            let f = OptimizedClosures.FSharpFunc<_, _, _>.Adapt (action)

            for i = 0 to str.Length - 1 do
                f.Invoke(i, str.[i])

    [<CompiledName("Map")>]
    let map (mapping: char -> char) (str: string) =
        if String.IsNullOrEmpty str then
            String.Empty
        else
            let result = str.ToCharArray()
            let mutable i = 0

            for c in result do
                result.[i] <- mapping c
                i <- i + 1

            new String(result)

    [<CompiledName("MapIndexed")>]
    let mapi (mapping: int -> char -> char) (str: string) =
        let len = length str

        if len = 0 then
            String.Empty
        else
            let result = str.ToCharArray()
            let f = OptimizedClosures.FSharpFunc<_, _, _>.Adapt (mapping)

            let mutable i = 0

            while i < len do
                result.[i] <- f.Invoke(i, result.[i])
                i <- i + 1

            new String(result)

    [<CompiledName("Filter")>]
    let filter (predicate: char -> bool) (str: string) =
        let len = length str

        if len = 0 then
            String.Empty

        elif len > LOH_CHAR_THRESHOLD then
            // By using SB here, which is twice slower than the optimized path, we prevent LOH allocations
            // and 'stop the world' collections if the filtering results in smaller strings.
            // We also don't pre-allocate SB here, to allow for less mem pressure when filter result is small.
            let res = StringBuilder()

            str
            |> iter (fun c ->
                if predicate c then
                    res.Append c |> ignore)

            res.ToString()

        else
            // Must do it this way, since array.fs is not yet in scope, but this is safe
            let target = Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked len
            let mutable i = 0

            for c in str do
                if predicate c then
                    target.[i] <- c
                    i <- i + 1

            String(target, 0, i)

    [<CompiledName("Collect")>]
    let collect (mapping: char -> string) (str: string) =
        if String.IsNullOrEmpty str then
            String.Empty
        else
            let res = StringBuilder str.Length
            str |> iter (fun c -> res.Append(mapping c) |> ignore)
            res.ToString()

    [<CompiledName("Initialize")>]
    let init (count: int) (initializer: int -> string) =
        if count < 0 then
            invalidArgInputMustBeNonNegative "count" count

        let res = StringBuilder count

        for i = 0 to count - 1 do
            res.Append(initializer i) |> ignore

        res.ToString()

    [<CompiledName("Replicate")>]
    let replicate (count: int) (str: string) =
        if count < 0 then
            invalidArgInputMustBeNonNegative "count" count

        let len = length str

        if len = 0 || count = 0 then
            String.Empty

        elif len = 1 then
            new String(str.[0], count)

        elif count <= 4 then
            match count with
            | 1 -> str
            | 2 -> String.Concat(str, str)
            | 3 -> String.Concat(str, str, str)
            | _ -> String.Concat(str, str, str, str)

        else
            // Using the primitive, because array.fs is not yet in scope. It's safe: both len and count are positive.
            let target =
                Microsoft.FSharp.Primitives.Basics.Array.zeroCreateUnchecked (len * count)

            let source = str.ToCharArray()

            // O(log(n)) performance loop:
            // Copy first string, then keep copying what we already copied
            // (i.e., doubling it) until we reach or pass the halfway point
            Array.Copy(source, 0, target, 0, len)
            let mutable i = len

            while i * 2 < target.Length do
                Array.Copy(target, 0, target, i, i)
                i <- i * 2

            // finally, copy the remain half, or less-then half
            Array.Copy(target, 0, target, i, target.Length - i)
            new String(target)

    [<CompiledName("ForAll")>]
    let forall predicate (str: string) =
        if String.IsNullOrEmpty str then
            true
        else
            let rec check i =
                (i >= str.Length) || (predicate str.[i] && check (i + 1))

            check 0

    [<CompiledName("Exists")>]
    let exists predicate (str: string) =
        if String.IsNullOrEmpty str then
            false
        else
            let rec check i =
                (i < str.Length) && (predicate str.[i] || check (i + 1))

            check 0
